package com.google.p2pbench;

import com.google.p2pbench.Connectivity.ConnectivityException;
import com.google.p2pbench.Connectivity.Peer;
import com.google.p2pbench.Connectivity.Socket;

import android.app.Activity;
import android.app.AlarmManager;
import android.app.AlertDialog;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.PowerManager;
import android.os.SystemClock;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MenuItem.OnMenuItemClickListener;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.ListView;
import android.widget.NumberPicker;
import android.widget.ToggleButton;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class MainActivity extends Activity {
  public static final String TAG = "P2PBench";
  private static final String EXTRA_SPEC = "com.google.P2PBench.spec";

  // NOTE: Trailing dot REQUIRED for NsdManager!
  // NOTE: Trailing dot FORBIDDEN for WifiP2pDnsSd!
  //    otherwise "malformed bonjour service response"
  // NOTE: _tcp or _udp required!
  // NOTE: _ prefix required!
  private static final String SERVICE_TYPE = "_adhoc-bench._tcp";

  private static final File BENCHMARK_FILE =
      new File(Environment.getExternalStorageDirectory(), "p2p-benchmark");
  private static final File RESULTS_FILE =
      new File(Environment.getExternalStorageDirectory(), "p2p-results");

  private static final int CHUNK_SIZE = 32000;
  private static final int[] BENCHMARK_SIZES = new int[] {
    10, 1000, 10000, CHUNK_SIZE, CHUNK_SIZE * 3, CHUNK_SIZE * 32, CHUNK_SIZE * 96};
  private static final String[] BENCHMARK_SIZE_STRINGS;

  static {
    BENCHMARK_SIZE_STRINGS = new String[BENCHMARK_SIZES.length];
    for (int i = 0; i < BENCHMARK_SIZES.length; ++i) {
      BENCHMARK_SIZE_STRINGS[i] = Integer.toString(BENCHMARK_SIZES[i]);
    }
  }

  // Handles responding to a Benchmark request.
  private static class BenchmarkRequestHandler implements Runnable {
    private final BenchmarkServer mServer;
    private final Socket mSocket;
    private final Thread mThread;

    BenchmarkRequestHandler(BenchmarkServer server, Socket socket) {
      mServer = server;
      mSocket = socket;
      mThread = new Thread(this, "BenchmarkRequestHandler");
      mThread.start();
    }

    @Override
    public void run() {
      try {
        DataInputStream din = new DataInputStream(mSocket.getInputStream());
        int length = din.readInt();
        mServer.recordRequest(length);

        DataOutputStream dout = new DataOutputStream(mSocket.getOutputStream());
        byte[] buffer = new byte[CHUNK_SIZE];
        while (length > 0) {
          int chunk = Math.min(buffer.length, length);
          dout.writeInt(chunk);
          dout.write(buffer, 0, chunk);
          length -= chunk;
        }
      } catch (ConnectivityException e) {
        mSocket.close();
      } catch (IOException e) {
        mSocket.close();
      }
    }

    public void stop() {
      mSocket.close();
    }
  }

  private class BenchmarkServer implements Connectivity.Listener, Connectivity.Service {
    private final String mTag;
    private final Connectivity mConn;
    private List<BenchmarkRequestHandler> mRequestHandlers
      = new ArrayList<BenchmarkRequestHandler>();

    BenchmarkServer(String tag, Connectivity conn) {
      mTag = tag;
      mConn = conn;
      log(mTag, "started");
      if (!mConn.connect(this))
        log(mTag, "Failed to connect");
    }

    public void recordRequest(int length) {
      recordEvent(String.valueOf(length));
    }

    private void recordEvent(String event) {
      Util.recordToFile(RESULTS_FILE, String.format("%s,%s,%s,%s\n", mTag,
          SystemClock.elapsedRealtime(), Util.getBatteryLevel(MainActivity.this), event));
    }

    @Override
    public void onMessage(Peer peer, ByteBuffer msg) {
      // TODO(szym): Auto-generated method stub
    }

    @Override
    public void onStream(final Peer peer, final Socket socket) {
      log(mTag, "stream from " + peer.getDescription());
      mRequestHandlers.add(new BenchmarkRequestHandler(this, socket));
    }

    @Override
    public void onConnected() {
      log(mTag, "connected");
      try {
        mConn.announce(SERVICE_TYPE, this);
      } catch (ConnectivityException exception) {
        log(mTag, "error " + exception.getMessage());
      }
      recordEvent("start");
    }

    @Override
    public void onError(ConnectivityException exception) {
      log(mTag, "error " + exception.getMessage());
      Log.e(TAG, "onError", exception);
      mConn.unnannounce(SERVICE_TYPE, this);
      List<BenchmarkRequestHandler> handlers;
      synchronized(this) {
        handlers = mRequestHandlers;
        mRequestHandlers = null;
      }
      if (handlers != null) {
        for (BenchmarkRequestHandler h : handlers)
          h.stop();
        recordEvent("stop " + mShutdownMode);
      }
      // TODO: what should be the next step?
    }
  }

  private class BenchmarkClient
      implements Connectivity.Listener, Connectivity.Discoverer, Runnable {
    private final String mTag;
    private final int mSize;
    private final Connectivity mConn;
    private Peer mPeer;
    private Thread mThread;
    private final long mTimeStarted;
    private long mTimeConnected;
    private long mTimeDiscovered;
    private long mTimeSocket;
    private long mTimeData;
    private long mTimeFinished;

    BenchmarkClient(String tag, int size, Connectivity conn) {
      mTag = tag;
      mSize = size;
      mConn = conn;
      log(mTag, "started " + size);
      mTimeStarted = SystemClock.elapsedRealtime();
      if (!mConn.connect(this))
        log(mTag, "Failed to connect");
    }

    private void recordResult() {
      StringBuilder builder = new StringBuilder(1024);
      builder.append(mTag);
      builder.append(',');
      builder.append(mTimeStarted);
      builder.append(',');
      builder.append(Util.getBatteryLevel(MainActivity.this));
      builder.append(',');
      builder.append(mSize);
      builder.append(',');
      builder.append(mTimeConnected - mTimeStarted);
      builder.append(',');
      builder.append(mTimeDiscovered - mTimeConnected);
      builder.append(',');
      builder.append(mTimeSocket - mTimeDiscovered);
      builder.append(',');
      builder.append(mTimeData - mTimeSocket);
      builder.append(',');
      builder.append(mTimeFinished - mTimeData);
      builder.append('\n');
      Util.recordToFile(RESULTS_FILE, builder.toString());
    }

    @Override
    public void onDiscovery(String serviceType, Peer peer) {
      if (mPeer != null) return;
      mTimeDiscovered = SystemClock.elapsedRealtime();
      mPeer = peer;
      log(mTag, "found " + serviceType + " at " + peer.getDescription());
      mThread = new Thread(this, "BenchmarkClient");
      mThread.start();
    }

    @Override
    public void run() {
      Socket socket;
      try {
        socket = mConn.getSocket(mPeer);
      } catch (ConnectivityException e) {
        log(mTag, "no socket " + e.getCause());
        return;
      }
      mTimeSocket = SystemClock.elapsedRealtime();
      // Don't cancel the benchmark now... give it another minute.
      mHandler.removeCallbacks(mCancelBenchmark);
      mHandler.postDelayed(mCancelBenchmark, 60000);
      try {
        DataOutputStream dout = new DataOutputStream(socket.getOutputStream());
        int length = mSize;
        dout.writeInt(length);
        DataInputStream din = new DataInputStream(socket.getInputStream());
        byte[] buffer = new byte[CHUNK_SIZE];
        while (length > 0) {
          int chunk = din.readInt();
          length -= chunk;
          if (mTimeData == 0) mTimeData = SystemClock.elapsedRealtime();
          while (chunk > 0) {
            int numread = din.read(buffer, 0, chunk);
            if (numread < 0) {
              chunk = -1;
              break;
            }
            chunk -= numread;
          }
          if (chunk < 0) break;
        }
        if (length == 0) {
          mTimeFinished = SystemClock.elapsedRealtime();
          log(mTag, "completed");
        }
      } catch (IOException e) {
        // ignore
        log(mTag, "exception " + e);
        Log.e(TAG, mTag, e);
      } catch (ConnectivityException e) {
        // ignore
        log(mTag, "error " + e.getMessage());
      }
      socket.close();
      recordResult();
      log(mTag, "done " + (mTimeFinished - mTimeStarted));
      onBenchmarkComplete();
    }

    @Override
    public void onConnected() {
      mTimeConnected = SystemClock.elapsedRealtime();
      log(mTag, "connected");
      mConn.discover(SERVICE_TYPE, this);
    }

    @Override
    public void onError(ConnectivityException exception) {
      if (mTimeFinished != 0) return;
      log(mTag, "error " + exception.getMessage());
      Log.e(TAG, "onError", exception);
      mConn.stopDiscovery(SERVICE_TYPE, this);
      onBenchmarkComplete();
    }
  }

  private LogListAdapter mLogAdapter;

  private Connectivity mConnectivity;

  private int mBenchmarkSize = BENCHMARK_SIZES[0];

  // Set if currently in auto mode.
  private boolean mAutoMode = false;

  private int mShutdownMode = Connectivity.SHUTDOWN_PREVIOUS;

  private AlarmManager mAlarmManager;
  private PowerManager.WakeLock mWakeLock;

  private NotificationManager mNotificationManager;

  private Runnable mToggleButtonsOff;
  private final Runnable mCancelBenchmark = new Runnable() {
    @Override
    public void run() {
      log(TAG, "Timed out");
      stopBenchmark(); // BenchmarkClient.onError will schedule another if needed.
    }
  };

  private final Handler mHandler = new Handler();

  private void log(final String tag, final String message) {
    Log.w(TAG, tag + ":" + message);
    runOnUiThread(new Runnable() {
      @Override
      public void run() {
        mLogAdapter.log(tag, message);
      }
    });
  }

  // Parses |spec| in expected format:
  // <conn> <shutdown> <mode> <param>
  // conn = WA|BT|WD|BATT
  // shutdown = on|off|previous
  // mode = TX|RX
  // param = size (TX), timeout (RX)
  private void startBenchmark(String spec) {
    String[] tokens = spec.split("[ ]+");
    String tech = tokens[0];
    String shutdown = tokens[1];
    String mode = tokens[2];
    String param = tokens.length > 3 ? tokens[3] : null;

    if ("WA".equals(tech)) {
      mConnectivity = new WifiAdhocConnectivity(this);
    } else if ("BT".equals(tech)) {
      mConnectivity = new BluetoothConnectivity(this);
    } else if ("WD".equals(tech)) {
      mConnectivity = new WifiDirectConnectivity(this);
    } else if ("BATT".equals(tech)) {
      Util.recordToFile(RESULTS_FILE, String.format("BATT,%s,%s",
          SystemClock.elapsedRealtime(), Util.getBatteryLevel(MainActivity.this)));
      return;
    } else {
      return;
    }

    mWakeLock.acquire();

    if ("off".equals(shutdown)) {
      mShutdownMode = Connectivity.SHUTDOWN_OFF;
    } else if ("on".equals(shutdown)) {
      mShutdownMode = Connectivity.SHUTDOWN_ON;
    } else {
      mShutdownMode = Connectivity.SHUTDOWN_PREVIOUS;
    }

    if ("TX".equals(mode)) {
      int size = mBenchmarkSize;
      if (param != null) size = Integer.parseInt(param);
      if (mAutoMode) mHandler.postDelayed(mCancelBenchmark, 20000);
      new BenchmarkClient(tech + "TX", size, mConnectivity);
    } else if ("RX".equals(mode)) {
      if (param != null) {
        // Have timeout.
        long delay = (int) (1000 * Float.parseFloat(param));
        mHandler.postDelayed(mCancelBenchmark, delay);
      }
      new BenchmarkServer(tech + "RX", mConnectivity);
    }
  }

  private void stopBenchmark() {
    mHandler.removeCallbacks(mCancelBenchmark);
    if (mConnectivity != null) {
      Connectivity conn;
      synchronized (this) {
        conn = mConnectivity;
        mConnectivity = null;
      }
      conn.shutdown(mShutdownMode);
      log(TAG, "stopped " + mShutdownMode);
      mWakeLock.release();
    }
  }

  private void scheduleAlarm() {
    String line = Util.popFirstLine(BENCHMARK_FILE);
    if (line == null) {
      // Assume the list is done.
      mAutoMode = false;
      Notification notification = new Notification.Builder(this)
        .setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE)
        .setAutoCancel(true)
        .setSmallIcon(R.drawable.ic_launcher)
        .setContentTitle("Benchmark complete")
        .setLights(0xFF00FF00, 300, 700)
        .build();
      mNotificationManager.notify(0, notification);
      return;
    }

    String[] parts = line.split("[ ]+", 2);
    long delay = (int) (1000 * Float.parseFloat(parts[0]));
    String spec = parts[1];

    Intent intent = new Intent(this, AlarmReceiver.class);
    intent.putExtra(EXTRA_SPEC, spec);
    long now = SystemClock.elapsedRealtime();
    mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, now + delay,
        PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT));
    log("Alarm", "Scheduled in " + delay);
  }

  private void stopAlarm() {
    Intent intent = new Intent(this, AlarmReceiver.class);
    mAlarmManager.cancel(
        PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT));
  }

  private void onBenchmarkComplete() {
    stopBenchmark();
    if (mAutoMode) {
      scheduleAlarm();
    } else {
      runOnUiThread(mToggleButtonsOff);
    }
  }

  @Override
  protected void onNewIntent(Intent intent) {
    super.onNewIntent(intent);
    String spec = intent.getStringExtra(EXTRA_SPEC);
    if (spec != null) {
      mAutoMode = true;
      startBenchmark(spec);
    } else {
      mAutoMode = false;
      stopAlarm();
    }
  }

  @Override
  protected void onResume() {
    super.onResume();
    mNotificationManager.cancel(0);
  }

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    mAlarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
    PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
    mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);

    mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

    setContentView(R.layout.activity_main);

    mLogAdapter = new LogListAdapter(getLayoutInflater());
    ListView logList = (ListView) findViewById(R.id.list_log);
    logList.setAdapter(mLogAdapter);

    final ToggleButton btnBtTx = (ToggleButton) findViewById(R.id.btn_bt_tx);
    final ToggleButton btnBtRx = (ToggleButton) findViewById(R.id.btn_bt_rx);
    final ToggleButton btnWaTx = (ToggleButton) findViewById(R.id.btn_wa_tx);
    final ToggleButton btnWaRx = (ToggleButton) findViewById(R.id.btn_wa_rx);
    final ToggleButton btnWdTx = (ToggleButton) findViewById(R.id.btn_wd_tx);
    final ToggleButton btnWdRx = (ToggleButton) findViewById(R.id.btn_wd_rx);

    OnCheckedChangeListener listener = new OnCheckedChangeListener() {
      @Override
      public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
        stopBenchmark();
        if (isChecked) {
          mAutoMode = false;
          mShutdownMode = Connectivity.SHUTDOWN_PREVIOUS;
          if (buttonView == btnBtTx) {
            startBenchmark("BT prev TX");
          } else if (buttonView == btnBtRx) {
            startBenchmark("BT prev RX");
          } else if (buttonView == btnWaTx) {
            startBenchmark("WA prev TX");
          } else if (buttonView == btnWaRx) {
            startBenchmark("WA prev RX");
          } else if (buttonView == btnWdTx) {
            startBenchmark("WD prev TX");
          } else if (buttonView == btnWdRx) {
            startBenchmark("WD prev RX");
          }
        }
      }
    };
    btnBtTx.setOnCheckedChangeListener(listener);
    btnBtRx.setOnCheckedChangeListener(listener);
    btnWaTx.setOnCheckedChangeListener(listener);
    btnWaRx.setOnCheckedChangeListener(listener);
    btnWdTx.setOnCheckedChangeListener(listener);
    btnWdRx.setOnCheckedChangeListener(listener);
    mToggleButtonsOff = new Runnable() {
      @Override
      public void run() {
        btnBtTx.setChecked(false);
        btnBtRx.setChecked(false);
        btnWaTx.setChecked(false);
        btnWaRx.setChecked(false);
        btnWdTx.setChecked(false);
        btnWdRx.setChecked(false);
      }
    };

    onNewIntent(getIntent());
  }

  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.main, menu);
    final Context context = this;
    menu.findItem(R.id.action_bench_size).setOnMenuItemClickListener(new OnMenuItemClickListener() {
      @Override
      public boolean onMenuItemClick(MenuItem item) {
        final NumberPicker picker = new NumberPicker(context);
        picker.setDescendantFocusability(NumberPicker.FOCUS_BLOCK_DESCENDANTS);
        picker.setMinValue(0);
        picker.setMaxValue(BENCHMARK_SIZE_STRINGS.length - 1);
        picker.setValue(Arrays.binarySearch(BENCHMARK_SIZES, mBenchmarkSize));
        picker.setDisplayedValues(BENCHMARK_SIZE_STRINGS);
        new AlertDialog.Builder(context).setView(picker)
            .setTitle("Benchmark Size")
            .setPositiveButton(android.R.string.ok, new OnClickListener() {
              @Override
              public void onClick(DialogInterface dialog, int which) {
                mBenchmarkSize = BENCHMARK_SIZES[picker.getValue()];
              }
            })
          .show();
        return true;
      }
    });
    menu.findItem(R.id.action_log_clear).setOnMenuItemClickListener(new OnMenuItemClickListener() {
      @Override
      public boolean onMenuItemClick(MenuItem item) {
        mLogAdapter.clear();
        return true;
      }
    });
    menu.findItem(R.id.action_start_auto).setOnMenuItemClickListener(new OnMenuItemClickListener() {
      @Override
      public boolean onMenuItemClick(MenuItem item) {
        stopAlarm();
        if (mAutoMode) {
          mAutoMode = false;
        } else {
          scheduleAlarm();
        }
        return true;
      }
    });
    return true;
  }

}
